package org.windowkit.toolkit.widget.treetable;

import java.util.ArrayList;
import java.util.List;

import javax.swing.SwingUtilities;
import javax.swing.tree.TreePath;

import org.ddevil.data.Data;
import org.ddevil.data.DisplayDescriptor;
import org.ddevil.data.KeyedChangeTracker;
import org.ddevil.data.SortOrder;
import org.ddevil.data.KeyedChangeTracker.KeyedChange;
import org.ddevil.data.set.BasicDataSet;
import org.ddevil.data.set.BasicSortableDataSet;
import org.ddevil.data.set.DataSet;
import org.ddevil.data.set.SortableDataSet;
import org.ddevil.data.util.DataSetUtils;
import org.ddevil.data.util.DataUtils;


/**
 * This is the default {@link TreeTableModel} implementation.
 *
 * @author Eric Lundin
 */
public class DefaultTreeTableModel extends AbstractTreeTableModel {
	protected SortableDataSet<Data> model;

	protected DSTreeNodeImpl ROOT_NODE = new DSTreeNodeImpl(-1, -1, -1, true,
			null);

	protected ArrayList<ArrayList<String>> keyList;

	protected DisplayDescriptor displayDescriptor;

	protected ArrayList<String> editableColumns;

	KeyedChangeTracker changeTracker;

	private static final int ROOT_NODE_INDEX_LEVEL = -1;

	private boolean autoClearChangeTracker = true;

	private ArrayList<ArrayList<String>> exportAttributes;

	public DefaultTreeTableModel(final DisplayDescriptor display,
			final ArrayList<ArrayList<String>> keys) {
		displayDescriptor = display;
		keyList = keys;
	}

	// TODO
	public void setData(final SortableDataSet ds) {
		model = ds;
		if (autoClearChangeTracker) {
			changeTracker.clear();
		}
		sortData();
	}

	private void sortData() {
		if (model != null) {
			model.clearAllSorts();
			for (ArrayList<String> level : keyList) {
				for (String sort : level) {
					model.setSort(sort, SortOrder.ASCENDING);
				}
			}
			fireTreeStructureChanged(this, new TreePath(ROOT_NODE));
		}
	}

	public void setAutoClearChangeTracker(final boolean auto) {
		autoClearChangeTracker = auto;
	}

	public void setEditableColumns(final ArrayList<String> editable) {
		editableColumns = editable;
	}

	/*
	 * (non-Javadoc)
	 *
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unused")
	public Class< ? > getColumnClass(final int column) {
		return TreeTableModel.class;
	}

	/*
	 * (non-Javadoc)
	 *
	 * {@inheritDoc}
	 */
	public int getColumnCount() {
		return displayDescriptor.size();
	}

	/*
	 * (non-Javadoc)
	 *
	 * {@inheritDoc}
	 */
	public String getColumnName(final int column) {
		return displayDescriptor.getAllLabels().get(column);
	}

	/*
	 * (non-Javadoc)
	 *
	 * {@inheritDoc}
	 *      int)
	 */
	public Object getValueAt(final Object node, final int column) {
		DSTreeNodeImpl currentNode = ((DSTreeNodeImpl) node);
		int row = currentNode.getRow();
		int dataLevel = currentNode.getDataLevel();
		int childLevel = Integer.MAX_VALUE;
		if (getChildCount(currentNode) != 0) {
			childLevel = ((DSTreeNodeImpl) getChild(currentNode, 0))
					.getDataLevel();
		}
		if (dataLevel > column || childLevel <= column) {
			return null;
		}
		return model.getDataAt(row, displayDescriptor.getDbIdAtIndex(column));
	}

	public ArrayList<String> getKeysForNode(final Object node) {
		if (((DSTreeNodeImpl) node).getLevel() == ROOT_NODE_INDEX_LEVEL) {
			throw new IllegalStateException(
					"Error root node should not be visible or selectable");
		}
		return keyList.get(((DSTreeNodeImpl) node).getLevel());
	}

	// TODO Remove this when we have a better system for leaving nodes expanded.
	public Data getKeyAttsForNode(final Object node) {
		return DataSetUtils.getAttributeSubset(getDataForNode(node)
				.exportRecord(0), buildKeyListForLevel(getLevel(node)));
	}

	public boolean deleteRow(final Data atts, final int lvl) {
		int level = lvl == -1 ? determineLevel(atts) : lvl;
		List<Integer> indices = DataSetUtils.getMatchIndicies(model, atts,
				buildKeyListForLevel(level));
		boolean foundRecord = false;
		int[] indicesArray = new int[indices.size()];
		boolean hadChildren;
		boolean update = level != 0
				&& DataSetUtils.getMatchIndicies(model, atts,
						buildKeyListForLevel(level - 1)).size() == 1;
		hadChildren = hasChild(atts, level);

		// If this is not the top level we cannot remove the entire row if this
		// row also represents a parent. In this case we need to null out the
		// fields which are part of the record at the level which is being
		// deleted. We probably don't need to disable sorting here since its the
		// last record in this level. If nothing else we can save time by not
		// making the data set sort everything we go to modify the data.
		if (update) {
			ArrayList<String> nullFields = buildKeyListAfterLevel(level - 1);
			model.setSortEnabled(false);
			for (int i = 0; i < nullFields.size(); i++) {
				model.setDataAt(indices.get(0), nullFields.get(i), null);
			}
			foundRecord = true && nullFields.size() > 0;
			model.setSortEnabled(true);
		}
		// We tell the dataset that this item has been removed for each level
		// below ourselves. Since by removing this record we remove all children
		// of this record.
		for (int i = indices.size() - 1; i >= 0; i--) {
			indicesArray[i] = indices.get(i);
		}
		if (!update) {
			foundRecord = true;
			model.removeRecords(indicesArray);
		}
		if (foundRecord) {
			changeTracker.itemRemoved(filterForChange(atts, level), level,
					hadChildren);
		}
		fireTreeStructureChanged(this, new TreePath(getRoot()));
		return foundRecord;
	}

	/**
	 * Returns the dataset rows for this node.
	 *
	 * @param node
	 * @return
	 */
	public DataSet getDataForNode(final Object node) {
		DataSet ds;
		if (node != null) {
			int[] indiciesArray;
			ArrayList<Integer> exportIndicies = new ArrayList<Integer>();
			getDataIndicies((DSTreeNodeImpl) node, exportIndicies);
			indiciesArray = new int[exportIndicies.size()];
			for (int i = 0; i < exportIndicies.size(); i++) {
				indiciesArray[i] = exportIndicies.get(i);
			}
			ds = model.exportRecords(indiciesArray);
		} else {
			ArrayList<String> allIds = new ArrayList<String>();
			for (ArrayList<String> current : keyList) {
				allIds.addAll(current);
			}
			ds = new BasicDataSet(allIds);
		}
		return ds;
	}

	public void getDataIndicies(final DSTreeNodeImpl node, final ArrayList<Integer> indicies) {
		if (!indicies.contains(node.getRow())) {
			indicies.add(node.getRow());
		}
		for (int i = 0; i < getChildCount(node); i++) {
			getDataIndicies((DSTreeNodeImpl) getChild(node, i), indicies);
		}
	}

	/*
	 * (non-Javadoc)
	 *
	 * {@inheritDoc}
	 *      int)
	 */
	@SuppressWarnings("unused")
	public boolean isCellEditable(final Object node, final int column) {
		boolean isEditable = getDataLevel(node) <= column
				&& editableColumns.contains(displayDescriptor.getAllIds().get(
						column));
		return isEditable;
	}

	/*
	 * (non-Javadoc)
	 *
	 * {@inheritDoc}
	 */
	public void setLabelsAndKeys(final DisplayDescriptor labels,
			final ArrayList<ArrayList<String>> keys) {
		displayDescriptor = labels;
		keyList = keys;
		changeTracker = new KeyedChangeTracker(keyList);
		if (exportAttributes == null) {
			exportAttributes = keys;
		}
	}

	public void setChangeExportKeys(final ArrayList<ArrayList<String>> exportAttribs) {
		exportAttributes = exportAttribs;
	}

	public void clearChanges() {
		changeTracker.clear();
	}

	/*
	 * (non-Javadoc)
	 *
	 * {@inheritDoc}
	 *      java.lang.Object, int)
	 */
	public void setValueAt(final Object aValue, final Object node, final int column) {
		// This is for modifies originated from the ui only for now.
		DSTreeNodeImpl currentNode = ((DSTreeNodeImpl) node);
		int row = currentNode.getRow();
		Data newVal;
		Data oldVal;
		String columnId = displayDescriptor.getDbIdAtIndex(column);
		ArrayList<String> key = new ArrayList<String>();
		int level = getLevel(node);
		boolean hadChildren = getChildCount(node) > 0;
		key = buildKeyListForLevel(level);
		oldVal = model.exportRecord(row);

		// Dont waste time making updates to unchanged data.
		if (!DataSetUtils.safeEquals(oldVal.getValue(columnId), aValue)) {
			List<Integer> indices = DataSetUtils.getMatchIndicies(model,
					oldVal, key);
			model.setSortEnabled(false);
			newVal = model.exportRecord(indices.get(0));
			newVal.setDataItem(columnId, aValue);
			fireValueChanging(oldVal, newVal, level);
			for (int currentRow : indices) {
				model.setDataAt(currentRow, columnId, aValue);
			}
			model.setSortEnabled(true);
			changeTracker.itemModified(filterForChange(oldVal, level),
					filterForChange(newVal, level), level, hadChildren);
			fireTreeStructureChanged(this, new TreePath(getRoot()));
			fireValueChanged();
			// Object[] path = buildPathToParent(currentNode);
			// fireTreeNodesChanged(this, buildPathToParent(currentNode),
			// getChildArray((DSTreeNode) path[path.length - 1]),
			// getChildrenArray((DSTreeNode) path[path.length - 1]));
		}
	}

	private Data filterForChange(final Data data, final int level) {
		return DataSetUtils.getAttributeSubset(data, buildExportKey(level));
	}

	private ArrayList<String> buildExportKey(final int level) {
		ArrayList<String> exportKey = new ArrayList<String>();
		for (int i = 0; i <= level; i++) {
			exportKey.addAll(exportAttributes.get(i));
		}
		return exportKey;
	}

	private void fireValueChanging(final Data oldValue,
			final Data newValue, final int level) {
		if (changeListener != null) {
			changeListener.saveTreeState(oldValue, newValue, level);
		}
	}

	private void fireValueChanged() {
		if (changeListener != null) {
			SwingUtilities.invokeLater(new Runnable() {
				public void run() {
					changeListener.restoreTreeState();
				}
			});
		}
	}


	private boolean hasChild(final Data atts, final int level) {
		boolean hasChild;
		if (level == keyList.size() - 1) {
			hasChild = false;
		} else {
			List<Integer> indices = DataSetUtils.getMatchIndicies(model, atts,
					buildKeyListForLevel(level));
			if (indices.size() > 1) {
				hasChild = true;
			} else if (indices.size() == 0) {
				hasChild = false;
			} else {
				ArrayList<String> notKeys = buildKeyListAfterLevel(level);
				hasChild = false;
				for (String current : notKeys) {
					if (!DataSetUtils.safeEquals(model.getDataAt(
							indices.get(0), current), null)) {
						hasChild = true;
						break;
					}
				}
			}
		}
		return hasChild;
	}

	/*
	 * (non-Javadoc)
	 *
	 * {@inheritDoc}
	 */
	public Object getChild(final Object parent, final int index) {
		DSTreeNodeImpl target = null;
		DSTreeNodeImpl parentNode = ((DSTreeNodeImpl) parent);
		int currentIndex = 0;
		if (parentNode.getRow() == ROOT_NODE_INDEX_LEVEL) {
			if (index == 0) {
				target = new DSTreeNodeImpl(0, 0, 0, keyList.size() > 1,
						getRoot());
			} else {
				for (int i = 0; i < model.size() && target == null; i++) {
					if (!keyValuesEqual(i, i + 1, keyList.get(0))) {
						currentIndex++;
					}
					if (currentIndex == index) {
						target = new DSTreeNodeImpl(i + 1, 0,
								createDataLevel(0), keyList.size() > 1,
								getRoot());
					}
				}
			}
		} else {
			target = new DSTreeNodeImpl(parentNode.getRow() + index, parentNode
					.getLevel() + 1,
					createDataLevel(parentNode.getLevel() + 1), parentNode
							.getLevel() + 1 == keyList.size(), parentNode);
		}
		return target;
	}

	private int createDataLevel(final int keyLevel) {
		ArrayList<String> keys = keyList.get(keyLevel);
		int index = -1;
		for (int i = 0; i < keys.size() && index == -1; i++) {
			if (displayDescriptor.containsId(keys.get(i))) {
				index = displayDescriptor.getIndexOfId(keys.get(i));
			}
		}
		return index;
	}

	/*
	 * (non-Javadoc)
	 *
	 * {@inheritDoc}
	 */
	public int getChildCount(final Object parent) {
		DSTreeNodeImpl parentNode = ((DSTreeNodeImpl) parent);
		int level = parentNode.getLevel();
		int parentRow = parentNode.getRow();
		int childCount = 0;
		if (!parentNode.isLeaf()) {
			if (parentNode.getRow() == ROOT_NODE_INDEX_LEVEL) {
				if (model == null || model.size() == 0) {
					return 0;
				}
				if (model.size() > 0) {
					childCount = 1;
				}
				for (int i = 0; i < model.size() - 1; i++) {
					if (!keyValuesEqual(i, i + 1, keyList.get(0))) {
						childCount++;
					}
				}
			} else {
				ArrayList<String> keys = new ArrayList<String>();
				for (int i = 0; i <= level; i++) {
					keys.addAll(keyList.get(i));
				}
				boolean foundAllChildren = false;
				childCount = 0;
				for (int currentRow = parentRow; !foundAllChildren
						&& currentRow < model.size(); currentRow++) {
					if (keyValuesEqual(parentRow, currentRow, keys)) {
						childCount++;
					} else {
						foundAllChildren = true;
					}
				}
			}
		}
		return childCount;
	}

	private boolean keyValuesEqual(final int row1, final int row2, final ArrayList<String> keys) {
		boolean isEqual = true;
		if (row1 >= model.size() || row2 >= model.size()) {
			isEqual = false;
		} else {
			for (int i = 0; i < keys.size(); i++) {
				Object val1 = model.getDataAt(row1, keys.get(i));
				Object val2 = model.getDataAt(row2, keys.get(i));
				if (!safeEquals(val1, val2)) {
					isEqual = false;
					break;
				}
			}
		}
		return isEqual;
	}

	private boolean safeEquals(final Object val1, final Object val2) {
		boolean equals;
		if (val1 == null && val2 == null) {
			equals = true;
		} else if (val1 == null ^ val2 == null) {
			equals = false;
		} else {
			equals = val1.equals(val2);
		}
		return equals;
	}

	/*
	 * (non-Javadoc)
	 *
	 * {@inheritDoc}
	 *      java.lang.Object)
	 */
	public int getIndexOfChild(final Object parent, final Object child) {
		DSTreeNodeImpl currentNode = ((DSTreeNodeImpl) parent);
		DSTreeNodeImpl childNode = ((DSTreeNodeImpl) child);
		int index = -1;
		ArrayList<String> keys = keyList.get(0);
		if (currentNode.getRow() == ROOT_NODE_INDEX_LEVEL) {
			for (int i = 0; i < model.size() && index == -1; i++) {
				if (keyValuesEqual(i, childNode.getRow(), keys)) {
					index = i;
				}
			}
		} else {
			index = currentNode.getRow() - childNode.getRow();
		}
		return index;
	}

	/*
	 * (non-Javadoc)
	 *
	 * {@inheritDoc}
	 */
	public DSTreeNodeImpl getRoot() {
		return ROOT_NODE;
	}

	/*
	 * (non-Javadoc)
	 *
	 * {@inheritDoc}
	 */
	public boolean isLeaf(final Object node) {
		return ((DSTreeNodeImpl) node).isLeaf();
	}

	/*
	 * (non-Javadoc)
	 *
	 * {@inheritDoc}
	 *      java.lang.Object)
	 */
	@SuppressWarnings("unused")
	public void valueForPathChanged(final TreePath path, final Object newValue) {

	}

	public class DSTreeNodeImpl implements DSTreeNode {
		private final int myRow;

		private final int myLevel;

		private final int dataLevel;

		private boolean isLeaf = false;

		Object myParent;

		public DSTreeNodeImpl(final int row, final int level, final int dlevel,
				final boolean allowsChildren, final DSTreeNodeImpl parent) {
			myRow = row;
			myLevel = level;
			dataLevel = dlevel;
			isLeaf = allowsChildren;
			myParent = parent;
		}

		public boolean isLeaf() {
			return !isLeaf;
		}

		public Object getParent() {
			return myParent;
		}

		public void setParent(final Object parent) {
			myParent = parent;
		}

		public int getLevel() {
			return myLevel;
		}

		public int getDataLevel() {
			return dataLevel;
		}

		public int getRow() {
			return myRow;
		}

		@Override
		public String toString() {
			Object value;
			String retVal = null;
			if (myRow != -1) {
				value = model.getDataAt(myRow, displayDescriptor
						.getDbIdAtIndex(dataLevel));
				if (value != null) {
					retVal = value.toString();
				}
			} else {
				retVal = "";
			}
			return retVal;
		}
	}

	/*
	 * (non-Javadoc)
	 *
	 * {@inheritDoc}
	 */
	public List<KeyedChange> getChanges() {
		return changeTracker.getChanges();
	}

	/*
	 * (non-Javadoc)
	 *
	 * {@inheritDoc}
	 */
	public void clearData() {
		// If we currently have no model guess at the identifiers from the key
		// set. Otherwise set our model as a new empty dataset with the current
		// sets ids.
		if (model == null) {
			ArrayList<String> allDbIds = new ArrayList<String>();
			for (int i = 0; i < keyList.size(); i++) {
				allDbIds.addAll(keyList.get(i));
			}
			model = new BasicSortableDataSet(new BasicDataSet(allDbIds));
		} else {
			model = new BasicSortableDataSet(new BasicDataSet(model
					.getIdentifiers().toArray(new String[0])));
		}
		if (autoClearChangeTracker) {
			changeTracker.clear();
		}
		fireTreeStructureChanged(this, new TreePath(ROOT_NODE));
	}

	/*
	 * (non-Javadoc)
	 *
	 * {@inheritDoc}
	 */
	public int getDataLevel(final Object node) {
		return ((DSTreeNodeImpl) node).getDataLevel();
	}

	/*
	 * (non-Javadoc)
	 *
	 * {@inheritDoc}
	 */
	public int getLevel(final Object node) {
		return ((DSTreeNodeImpl) node).getLevel();
	}

	/*
	 * (non-Javadoc)
	 *
	 * {@inheritDoc}
	 */
	public DisplayDescriptor getDisplayDescriptor() {
		return displayDescriptor;
	}

	/*
	 * (non-Javadoc)
	 *
	 * {@inheritDoc}
	 */
	public int getIndexOfId(final String dbId) {
		return displayDescriptor.getIndexOfId(dbId);
	}

	private int determineLevel(final Data atts) {
		boolean foundNonNull;
		int level = 0;
		for (int i = 0; i < keyList.size(); i++) {
			foundNonNull = false;
			for (String key : keyList.get(i)) {
				if (!safeEquals(atts.getValue(key), null)) {
					foundNonNull = true;
				}
			}
			if (foundNonNull) {
				level = i;
			} else {
				break;
			}
		}
		return level;
	}

	public ArrayList<String> buildKeyListForLevel(final int lvl) {
		ArrayList<String> keys = new ArrayList<String>();
		for (int i = 0; i <= lvl; i++) {
			keys.addAll(keyList.get(i));
		}
		return keys;
	}

	private ArrayList<String> buildKeyListAfterLevel(final int lvl) {
		ArrayList<String> notKeys = new ArrayList<String>();
		for (int i = keyList.size() - 1; i > lvl; i--) {
			notKeys.addAll(keyList.get(i));
		}
		return notKeys;
	}

	/*
	 * (non-Javadoc)
	 *
	 * {@inheritDoc}
	 */
	public void addRecord(final Data record) {
		int level = determineLevel(record);

		List<Integer> indices;
		boolean isDuplicate = DataSetUtils.getMatchIndicies(model, record,
				buildKeyListForLevel(level)).size() > 0;
		if (!isDuplicate) {
			boolean update = level != 0
					&& DataSetUtils.getMatchIndicies(model, record,
							buildKeyListForLevel(level - 1)).size() == 1;

			// If this is the first record under the assumed parent we need to
			// update the parent fields since its values for this level will be
			// null
			// as it has no child.
			// Otherwise import a new record.
			if (update) {
				ArrayList<String> notKeys = buildKeyListAfterLevel(level - 1);
				indices = DataSetUtils.getMatchIndicies(model, record,
						buildKeyListForLevel(level - 1));
				boolean hasChild = false;
				for (String key : notKeys) {
					if (!DataUtils.safeEquals(model.getDataAt(indices
							.get(0), key), null)) {
						hasChild = true;
						break;
					}
				}
				if (hasChild) {
					model.importRecord(record);
				} else {
					model.setSortEnabled(false);
					for (int i = 0; i < notKeys.size(); i++) {
						model.setDataAt(indices.get(0), notKeys.get(i), record
								.getValue(notKeys.get(i)));
					}
					model.setSortEnabled(true);
				}
			} else {
				model.importRecord(record);
			}
			changeTracker.itemAdded(filterForChange(record, level), level);
			// DSTreeNode[] pathToNode = buildPathToNode(record, level);
			// DSTreeNode[] pathToParent = new DSTreeNode[pathToNode.length -
			// 1];
			// System.arraycopy(pathToNode, 0, pathToParent, 0,
			// pathToNode.length -
			// 1);
			// fireTreeNodesInserted(this, pathToParent,
			// getChildArray(pathToParent[pathToParent.length - 1]),
			// getChildrenArray(pathToParent[pathToParent.length - 1]));
			fireTreeStructureChanged(this, new TreePath(getRoot()));
		}
	}

	/*
	 * (non-Javadoc)
	 *
	 * {@inheritDoc}
	 *      org.ddevil.data.Data)
	 */
	public boolean modifyRecord(final Data oldValue, final String dbId,
			final Object value, final int lvl) {
		boolean foundRecord = false;
		int level = -1;
		if (lvl == -1) {
			for (int i = 0; i < keyList.size(); i++) {
				if (keyList.get(i).contains(dbId)) {
					level = i;
					break;
				}
			}
		} else {
			level = lvl;
		}
		ArrayList<String> keys = buildKeyListForLevel(level);
		List<Integer> indices = DataSetUtils.getMatchIndicies(model, oldValue,
				keys);
		if (indices.size() > 0) {
			model.setSortEnabled(false);
			for (int i = 0; i < indices.size(); i++) {
				model.setDataAt(indices.get(i), dbId, value);
			}
			Data newValue = model.exportRecord(indices.get(0));
			changeTracker.itemModified(filterForChange(oldValue, level),
					filterForChange(newValue, level), level, hasChild(oldValue,
							level));
			model.setSortEnabled(true);
			foundRecord = true;
			fireTreeStructureChanged(this, new TreePath(getRoot()));
		}
		return foundRecord;
	}

	public interface DSTreeNode {

		public boolean isLeaf();

		public int getLevel();

		int getDataLevel();

		public Object getParent();

		int getRow();

		public String toString();

		public void setParent(Object parent);
	}

	// TODO Temp for now. Need to come up with better way. i.e. dont use a
	// sorted data set.
	public interface TreeTableModelStateListener {
		public void saveTreeState(Data oldVal, Data newVal,
				int level);

		public void restoreTreeState();
	}

	public void setTreeTableModelChangeListener(final TreeTableModelStateListener l) {
		if (l != null) {
			changeListener = l;
		}
	}

	protected TreeTableModelStateListener changeListener;
}
